// Copyright (C) 2007 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*****************************************************************************
 * Automatically inserts a doc-level TOC based on the <H2> (and possibly <H3>)
 * elements in the document. The script runs when the page loads.
 *
 * In brief, to use this script, put something like this:
 *       <DIV ID="nav" CLASS="SOMECLASS"></DIV>
 * where you want the TOC to show up. The choices for SOMECLASS are:
 *    nav
 *    nav-2-levels, nav-2-levels-inside
 *    nav-2-columns, nav-2-columns-inside
 *    nav-3-columns, nav-3-columns-inside
 *    nav-n-levels, nav-n-levels-inside
 *
 * Basically, the differences are: 
 * 
 * 1. Which elements: nav includes just H2 elements. The others all include both
 * H2 & H3 elements.
 *
 *    2. Formatting: nav, nav-2-levels* and nav-n-levels all create a 1-column
 * list.  nav-2-columns* and nav-3-columns* create 2- and 3- column lists.
 * (There's a little more detail about this later.)
 *
 *    3. For everything other than nav, there's a class FOO and a class
 * FOO-inside. The difference between these classes has to do with where the
 * script looks for the Hx elements. For FOO, the script includes the Hx
 * elements that are direct descendants of BODY. For FOO-inside it include Hx
 * elements that are direct descendants of the element that contains the <div
 * id="nav"...> element. So, if you have:
 *    <body>
 *      A
 *      <div id="content">
 *        B
 *        <div id="nav"></div>
 *        C
 *      </div>
 *      D
 * and you want the Hx elements in the A & C areas, use FOO. If you want the
 * elements in B & C, use FOO-inside.
 * Another common look is simply:
 *    <body>
 *     A
 *     <div id="nav"></div>
 *     B
 * In this case, just use FOO.
 *
 *** TOC FORMATS ***
 * 
 * The TOC can contain all the H2s in the document or both H2s and H3s.  Your
 * choices are:
 *
 * 1. TO: Create a TOC with just <H2> elements, 
 *        appearing immediately after the <H1>. 
 *    YOU: Put nothing in your document.
 *    DETAILS: The TOC is a <UL>.
 *             NOT RECOMMENDED--Some browsers (such as Firefox) can mess up
 *             the formatting.
 *
 * 2. TO: Create a TOC with just <H2> elements, 
 *        appearing where you want it.
 *    YOU: Put <DIV ID="nav" CLASS="nav"></DIV> in your document.
 *         id="nav" is required; class="nav" is optional.
 *    DETAILS: The TOC is a <UL>. It  appears as the content of that <DIV>.
 *
 * 3. TO: Create a TOC with both <H2> and <H3> elements, as a single list.
 *    YOU: Put <DIV ID="nav" CLASS="nav-2-levels"></DIV> in your document.
 *         Both id="nav" and class="nav-2-levels" are required.
 *    DETAILS: The TOC is a <DL>.
 *             The <H2> entries become <DT> elements.
 *             The <H3> entries become <DD> elements.
 *
 * 4. TO: Create a TOC with both <H2> and <H3> elements, as a 2-column table. 
 *    YOU: Put <DIV ID="nav" CLASS="nav-2-columns"></DIV> in your document.
 *         Both id="nav" and class="nav-2-columns" are necessary.
 *    DETAILS: The TOC is a <DL>.
 *             The <H2> entries become <DT> elements.
 *             The <H3> entries become <DD> elements.
 *
 * 5. TO: Create a TOC with both <H2> and <H3> elements, as a 3-column table. 
 *    YOU: Put <DIV ID="nav" CLASS="nav-3-columns"></DIV> in your document.
 *         Both id="nav" and class="nav-3-columns" are necessary.
 *    DETAILS: As for nav-2-columns.
 *
 * 6. TO: Create a TOC with all header elements <H2>-<H6>, as a single list.
 *    YOU: Put <DIV ID="nav" CLASS="nav-n-levels"></DIV> in your document.
 *         Both id="nav" and class="nav-n-levels" are required.
 *    DETAILS: The TOC is a <DL>.
 *             The <H*> entries become <DD> elements.
 *             Every level down becomes a new nested <DL> element.
 *
 *** CAVEATS ***
 *
 *  -- Tries to use any embedded '<h2><a name="foo"></a></h2>' tags for
 *     linking. If they do not exist, it uses the <H2 ID="foo"></H2> attribute.
 *
 *  -- It's best if your HTML provides its own ID attributes. However, if an
 *     <H2>, <H3> element does not have an ID attribute, the script creates
 *     one. It forms IDs named "s1", "s2", ..., so if you've already get IDs
 *     with those names, there might be conflicts.
 * 
 *
 *** IMPLEMENTATION ***
 * 
 * Once we've decided which type of TOC we're creating:
 *
 * We scan for all appropriate elements with an id= attribute.  The
 * <h2or3>...</h2or3> text is then used for the navigation text, and the URL is
 * set to #id.  If you set a title="..." attribute on the h2 or h3, that is used
 * instead.
 *
 *****************************************************************************/

NB_addEvent(window, "load", NB_navbarInit);

// NB_addEvent function by By Scott Andrew -- from sorttable.js
function NB_addEvent(elm, evType, fn, useCapture)
{
  if (elm.addEventListener){
    elm.addEventListener(evType, fn, useCapture);
    return true;
  } else if (elm.attachEvent){
    var r = elm.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be removed");
  }
}

/*** Jump to Current Anchor ***
 *
 * Browsers have a bug where a link to an anchor on a page doesn't jump to
 * the right line.  It jumps to the wrong place on the page because the
 * browser computes the line to jump to before the dynamic TOC is inserted.
 *
 * NB_jumpToCurrentAnchor() realigns the window to an anchor selected
 * in the URL, if any.  Call this after JavaScript that adds elements
 * to the page has been executed.
 */
function NB_jumpToCurrentAnchor() {
 var indexOfAnchor = window.location.toString().indexOf("#");
 if (indexOfAnchor != -1) {
   window.location = window.location.toString().substring(indexOfAnchor);
 }
}

// Find an existing navbar.
// Decide what type of TOC we're creating and call the appropriate function.
function NB_navbarInit() {
  if (!document.getElementsByTagName) return; // no browser support

  // Find the navigation bar
  var navbar = NB_findNavbar();
  if (navbar == null) return; // could not create a navbar


  // Figure out what type of TOC we want and go create it.
  var cl = navbar.className;

  if (cl == "nav-3-columns") {
    NB_create3ColumnTOC (navbar);
  } else if (cl == "nav-3-columns-inside") {
    NB_create3ColumnTOC (navbar, true);
  } else if (cl == "nav-2-columns") {
    NB_create2ColumnTOC (navbar);
  } else if (cl == "nav-2-columns-inside") {
    NB_create2ColumnTOC (navbar, true);
  } else if (cl == "nav-2-levels") {
    NB_create2LevelTOC (navbar);
  } else if (cl == "nav-2-levels-inside") {
    NB_create2LevelTOC (navbar, true);
  } else if (cl == "nav-n-levels") {
    NB_createNLevelTOC (navbar);
  } else if (cl == "nav-n-levels-inside") {
    NB_createNLevelTOC (navbar, true);
  } else {
    NB_create1LevelTOC (navbar);
  }

  NB_jumpToCurrentAnchor();
}


// Creates a single-level TOC as a list.
function NB_create1LevelTOC (navbar) {

  // Create (or retrieve) the top-level element of the TOC.
  var toc = NB_findNavbarSections(navbar, "ul");

  // Find all <h2 id="...">...</h2> sections
  // Because we know we want exactly the <H2> elements, we can
  // use the zippy getElementsByTagName function. 
  var headers = document.getElementsByTagName("h2");

  // Put each header in the TOC
  NB_putHeadersIn(toc, headers, 1);
  navbar.appendChild(toc);
}

// Creates a 2-level TOC as a single list.
function NB_create2LevelTOC (navbar, headsInside) {
  // Create (or retrieve) the top-level element of the TOC.
  var toc = NB_findNavbarSections(navbar, "dl");

  // Find all <H2> and <H3> elements
  var headers = NB_findHNodes(navbar, headsInside, 2, 3);

  // Put each header in the TOC
  NB_putHeadersIn(toc, headers, 2);
  navbar.appendChild(toc);
}

// Creates a 2-level TOC (H2 & H3) as a 2-column table.
function NB_create2ColumnTOC (navbar, headsInside) {

  // Create (or retrieve) the top-level element of the TOC.
  var toc = NB_findNavbarSections(navbar, "dl");

  // Find all <H2> and <H3> elements
  var headers = NB_findHNodes(navbar, headsInside, 2, 3);

  // Put each header in the TOC.
  NB_putHeadersIn(toc, headers, 2);
  // Now transform the single DL into a 2-column table
  NB_makeTableTOC(navbar, toc, 2);

}

// Creates a 2-level TOC (H3 & H3) as a 3-column table.
function NB_create3ColumnTOC (navbar, headsInside) {

  // Create (or retrieve) the top-level element of the TOC.
  var toc = NB_findNavbarSections(navbar, "dl");

  // Find all <H2> and <H3> elements
  var headers = NB_findHNodes(navbar, headsInside, 2, 3);

  // Put each header in the TOC.
  NB_putHeadersIn(toc, headers, 2);
  // Now transform the single DL into a 3-column table
  NB_makeTableTOC(navbar, toc, 3);
}


function NB_isH1(node) {
  var res = ((node.tagName == "h1") || (node.tagName == "H1"));
  return res;
}

function NB_isH2(node) {
  var res = ((node.tagName == "h2") || (node.tagName == "H2"));
  return res;
}

function NB_isH3(node) {
  var res = ((node.tagName == "h3") || (node.tagName == "H3"));
  return res;
}

function NB_isH4(node) {
  var res = ((node.tagName == "h4") || (node.tagName == "H4"));
  return res;
}

function NB_isH5(node) {
  var res = ((node.tagName == "h5") || (node.tagName == "H5"));
  return res;
}

function NB_isH6(node) {
  var res = ((node.tagName == "h6") || (node.tagName == "H6"));
  return res;
}

function NB_isH(node) {
  var res = (NB_isH1(node) || NB_isH2(node) || NB_isH3(node) ||
             NB_isH4(node) || NB_isH5(node) || NB_isH6(node));
  return res;
}

function NB_headerLevel(node) {
  if (NB_isH1(node)) return 1;
  if (NB_isH2(node)) return 2;
  if (NB_isH3(node)) return 3;
  if (NB_isH4(node)) return 4;
  if (NB_isH5(node)) return 5;
  if (NB_isH6(node)) return 6;
  return 0;
}

function NB_putHeadersIn(toc, headers, numLevels) {
  for (var i = 0; i != headers.length; i++) {
    var header = headers[i];
    
    // Make sure the elements each have an ID.
    if (header.id == "" || header.id == null) {
      // Create a synthetic id
      header.id = "s" + (i+1);
    }

    // The link target defaults to the header id attribute
    anchor_target = header.id;
    // If the header starts with an <a name=""> tag, use that instead
    if (header.firstChild.nodeName == "A" && header.firstChild.name != "") {
      anchor_target = header.firstChild.name;
    }

    // Create the element holding the TOC entry & put in place.
    var text = NB_nodeToText(header);
    
    if (text != "") {
      
      // First create the contents of the item as  <a href>...</a>
      var textNode = document.createTextNode(text);
      
      var link = document.createElement("a");
      link.href = "#" + anchor_target;
      link.appendChild(textNode);
      
      // Then create the item itself
      var item;
      
      // Type of item depends on how many levels total
      // and H2 versus H3.
      if (numLevels == 1) {
	item = document.createElement("li");
      } else if (NB_isH2(header)) {
	item = document.createElement("dt");
      }
      else {
	item = document.createElement("dd");
      }
      
      item.appendChild(link);
      toc.appendChild(item);
    }
  }
}

// This returns a text string containing all the text in the node.
function NB_nodeToText(node) {
  var TEXT_NODE = 3; // Defined in Mozilla but not MSIE :(
  
  var text = "";
  for (var j = 0; j != node.childNodes.length; j++) {
    var child = node.childNodes[j];
    if (child.nodeType == TEXT_NODE) {
      if (child.nodeValue != "[Top]") { //ok, that's a hack, but it works.
	text = text + child.nodeValue;
      }
    } else {
      text = text + NB_nodeToText(child);
    }
  }
  return text;
}


// Take a single DL and turn it into a table with numColumns columns
function NB_makeTableTOC(navbar, toc, numColumns) {

  // Now you've got a single TOC element. See how many children it has, split it
  // into 2 or 3 elements, and create a 1-row table with each element a cell in
  // that table.

  var tocChildren = toc.childNodes;
  var tocLength = tocChildren.length;
  var col1End = Math.floor(tocLength / numColumns);
  var col2End = (numColumns == 2) ? tocLength : col1End * 2;

  // Create the lists that go into columns
  var dl1 = document.createElement("dl");
  var dl2 = document.createElement("dl");
  var dl3 = (numColumns == 3) ? document.createElement("dl") : null;
  var thisChild;

  // No doubt there's some much cleaner way to do this, but if it works...  The
  // trick is appendChild moves the node from where it was to where you put it!

  for (var i = 0; i < tocLength; i++) {
    if (i < col1End) {
      dl1.appendChild(tocChildren[0]);
    } else if (i < col2End) {
      dl2.appendChild(tocChildren[0]);
    } else {
      // only get here if 3 columns
      dl3.appendChild(tocChildren[0]);
    }
  }

  var tocTable = document.createElement("table");
  navbar.appendChild(tocTable);
  tocTable.className = "unruled";
  var tocBody = document.createElement("tbody");
  tocTable.appendChild(tocBody);

  var tocRow = document.createElement("tr");
  tocBody.appendChild(tocRow);

  // 1st column
  var tocCell = document.createElement("td");
  tocCell.className = "first";
  tocRow.appendChild(tocCell);
  tocCell.appendChild(dl1);

  // 2nd column
  tocCell = document.createElement("td");
  tocRow.appendChild(tocCell);
  tocCell.appendChild(dl2);
  
  // 3rd column
  if (numColumns == 3) {
    tocCell = document.createElement("td");
    tocRow.appendChild(tocCell);
    tocCell.appendChild(dl3);
  }
}



// See if there's a place for the navbar. 
// If not, create one after the H1.
function NB_findNavbar() {
  var navbar = document.getElementById("nav");
  if (navbar == null) {
    // Try to insert it after the <h1>
    var h1s = document.getElementsByTagName("h1");
    if (h1s.length >= 1) {
      navbar = document.createElement("div");
      navbar.id = "nav";
      h1s[0].parentNode.insertBefore(navbar, h1s[0].nextSibling);
    }
  }
  return navbar;
}

// Keep whatever is in the navbar. Identify an existing TOC by id="sections".
function NB_findNavbarSections(navbar, type) {
  var toc = document.getElementById("sections");
  if (toc == null) {
    toc = document.createElement(type);
    toc.id = "sections";
  }
  return toc;
}

function NB_createNLevelTOC (navbar, headsInside) {
  // Create (or retrieve) the top-level element of the TOC.
  var toc = NB_findNavbarSections(navbar, "dl");

  // Find all the nodes that might be headings.
  var headers = NB_findAllHNodes(navbar, headsInside);

  for (var i = 0; headers[i]; i++) {
    var header = headers[i];

    // Make sure the elements each have an ID.
    if (header.id == "" || header.id == null) {
      // Create a synthetic id
      header.id = "s" + (i+1);
    }
  }

  for (var k = 0; k < headers.length;) {
    var l = NB_putOneLevelOfHeadersIn(toc, headers, k, 2);
    if( l == k ) l++;
    k = l;
  }

  navbar.appendChild(toc);
}

function NB_putOneLevelOfHeadersIn(toc, headers, i, depth) {
  for (var j = i; j < headers.length; ) {
    var header = headers[j];
    var hd = NB_headerLevel(header);
    if( 0 == hd ) {
      return j+1;
    }

    if( hd < depth ) {
      // Done with our level
      return j;
    }

    if( hd > depth ) {
      // Nest a level
      var item = document.createElement("dd");
      var itemList = document.createElement("dl");
      j = NB_putOneLevelOfHeadersIn(itemList, headers, j, depth+1);
      item.appendChild(itemList);
      toc.appendChild(item);
      continue;
    }

    // Create the element holding the TOC entry & put in place.
    var text = NB_nodeToText(header);
    
    if (text != "") {
      
      // First create the contents of the item as  <a href>...</a>
      var textNode = document.createTextNode(text);
      
      var link = document.createElement("a");
      link.href = "#" + header.id;
      link.appendChild(textNode);
      
      // Then create the item itself
      var item;
      
      item = document.createElement("dd");

      item.appendChild(link);
      toc.appendChild(item);
    }

    j++;
  }
}

// Find header nodes within the element that contains the navbar.
// The min and max parameters specify which level of headers you want.
// For example, min=1 and max=3 will return h1, h2, and h3 nodes.  Use
// NB_findAllNodes if you want all of the headers.
function NB_findHNodes(navbar, headsInside, min, max) {
  var hNodes = new Array();
  if (headsInside) {
    NB_findHNodesRecursive(navbar.parentNode.childNodes, hNodes, min, max);
  } else {
    NB_findHNodesRecursive(document.body.childNodes, hNodes, min, max);
  }
  return hNodes;
}

function NB_findAllHNodes(navbar, headsInside) {
  return NB_findHNodes(navbar, headsInside, 1, 6);
}

function NB_findHNodesRecursive(nodeList, hNodes, min, max) {
  for (var i=0; i < nodeList.length; i++) {
    var level = NB_headerLevel(nodeList[i]);
    if (level >= min && level <= max) {
      hNodes.push(nodeList[i]);
    }
    if (nodeList[i].childNodes) {
      NB_findHNodesRecursive(nodeList[i].childNodes, hNodes, min, max);
    }
  }
}
